Skip to content

lint: widen the vocabulary gate's face refusal to every published package and retire boot-refusal - #17056

Draft
os-bill wants to merge 8 commits into
mainfrom
claude/issue-16649-widen-vocabulary-gate-retire-boot-refusal
Draft

lint: widen the vocabulary gate's face refusal to every published package and retire boot-refusal#17056
os-bill wants to merge 8 commits into
mainfrom
claude/issue-16649-widen-vocabulary-gate-retire-boot-refusal

Conversation

@os-bill

@os-bill os-bill commented Sep 9, 2026

Copy link
Copy Markdown
Collaborator

Fixes #16649

Second half of the card. The first half (registering the fourteen door: 'none' codes) landed as #16879 on 2026-09-08; this is the mechanical closure the card's Scope and triage acceptance items 3/4/5 ask for.

Implemented per the director seat's ruling of 2026-09-08 (comment 5586454647, decision batch #95), quoted verbatim in the card. The later release comment 5586642296 re-lists options A/B/C as open; it was written thirteen minutes after the ruling and does not cite it. This PR follows the ruling; A/B/C were not re-litigated.

What changed

scripts/check-dispatcher-error-vocabulary.mjs — the face refusal, which asked file.startsWith('packages/spec/src/') and nothing else, now asks which published package's src/ a site sits in. Members are enumerated from their manifests through scripts/workspace-enumerator.mjs, the repo's one parse of pnpm-workspace.yaml; "published" is name && private !== true, the same test check-published-files.mjs applies, because the ruling's premise is about what a consumer installs. Measured on this tree: 80 workspace members, 70 published, 69 of those with a src/ on disk. The derivation emits one prefix per published member and does not probe the disk for src/, so the printed count is members, not members-with-sources; that is stated where it is derived.

The two faces stay separate because they differ: packages/spec/src/ keeps foreign-vocabulary / runtime-pinned only, and the widened region additionally admits pending-registration under a named, dated allowance owed to #8846. The spec region keeps reporting spec-face-unregistered and the widened region reports a new published-face-unregistered, so the ledger's existing prose about that kind stays true and the two regions stay distinguishable in output.

The enumeration is floor-checked before use (PUBLISHED_SOURCE_FACE_FLOOR = 40, against 70 measured). A face list that silently shrank would turn the refusal off for whatever fell out and print the same green line as a complete one, so the gate refuses instead of reporting.

The unclassified-site remedy text told every author to reach for pending-registration. Inside packages/spec/src/ that is the one verdict the face refuses, so the text is now face-aware and names the ledger row first.

packages/runtime/src/dispatcher-error-vocabulary.tsboot-refusal retired from CodeVerdict. With the whole scan population inside the face (52 sites: 47 published-face, 5 spec-face), no row can legally carry it. Its own doc comment had already named this as the condition for its retirement. A paragraph in its place records what it meant and why it must not come back; the running log records the fourth cycle closing mechanically rather than by having been emptied once.

The ledger TSDoc, corrected (patch round)

packages/spec ships src/**/*.zod.ts in its files[], so error-code-ledger.zod.ts's TSDoc is published prose, and retiring the verdict made part of it false rather than merely stale. Two blocks changed, both inside the :100-132 docblock; the two regions contended by #17015 and #16783 were not touched.

:109-113 — false, and fixed. It said the dispatcher vocabulary's boot-refusal verdict records reachability for codes not yet registered, and pointed at the file this branch deletes it from. That describes where a live mechanism lives, not a case that can no longer arise, so a reader would have followed the pointer and found nothing. It now records the retirement and names what replaced it: the gate refuses every verdict that parks a site away from registration under any published package's src/, so a door: 'none' code has no resting place short of a ledger row.

:119-124 — also changed, and here is why. The vacuous-disjunct argument does hold for it: with the verdict gone, "a boot-refusal or pending-registration row is a finding" keeps a true half. But that is not the only thing the paragraph claims. It opened packages/spec/src/** is held to this mechanically, which after this PR understates what is enforced — a reader would conclude only the spec tree is guarded, which is exactly the "守了一部分 reads the same as 守住了" failure this card exists to close. So it now states the published face, the stricter spec sub-face and the dated #8846 allowance, with both finding kinds named.

The mirrored reference page content/docs/references/api/error-code-ledger.mdx was regenerated with check:generated --fix (gen:docs), never hand-edited.

The eight other boot-refusal mentions in that file (:170, :500, :798, :824, :1060, :1105, :1126, :1203) were read and left: every one is past tense about what a now-deleted row recorded and ratcheted out, with the reading itself stated to live on the ledger row now. None asserts a live mechanism.

Acceptance item 4: the red-before-green demonstration

Item 4 was written when the fourteen codes were unregistered, so "run the widened gate and watch it red on all fourteen" was the available proof. git grep -c "verdict: 'boot-refusal'" now returns 0 — the first half consumed that proof. The equivalent was constructed instead, and it measures the delta rather than merely showing the gate can fail: one mutation, two gates, the same tree.

packages/types/src/node.ts's MODULE_NOT_FOUND row (a published package outside packages/spec/src/) was flipped from foreign-vocabulary to sandbox-authored — still a legal CodeVerdict, so nothing else catches it.

gate source exit
base origin/main's script, byte-for-byte (308374 bytes), run from this worktree's scripts/ so its ROOT resolves here 0 — "52 unregistered code-stamping site(s), all classified"
widened this branch 1[published-face-unregistered] packages/types/src/node.ts stamps unregistered code 'MODULE_NOT_FOUND' (objlit) ... classifies it 'sandbox-authored' — a verdict refused under a published package's src/

Mutation proven on disk before either leg: HEAD blob 0a1e68f2..., mutated blob 26b59e1e..., anchor count 1 to 0 and injected count 0 to 1. Restored under trap ... EXIT INT TERM with git checkout HEAD -- ABSOLUTE_PATH; restored blob equals the HEAD blob, git diff HEAD --stat and git status --porcelain both empty. The temporary base-gate copy was removed by the same trap.

Two further ablations, same discipline:

  • The allowance is load-bearing, and the ruling's stated reason for it is true on this tree. Deleting 'pending-registration' from the widened verdict set (the rule applied verbatim, with no carve-out) reds exactly two rows, and they are exactly the two the ruling named: AMBIGUOUS_METADATA_STEM in packages/metadata and owd_widening_forbidden in packages/plugins/plugin-security. Gate exit 1, restored blob equals HEAD.
  • The floor refuses rather than going quiet. Raising it to 999 makes the gate throw and name the enumerated count. Gate exit 1, restored blob equals HEAD.

Acceptance item 5: the negative controls

The two surviving verdicts must not be refused by the widening, or the implementation would be "everything unregistered reds", which passes item 4 while deleting two legitimate exemptions. On the live tree the widened gate is green over all 52 rows — 49 foreign-vocabulary, 1 runtime-pinned, 2 pending-registration — and the --self-test battery pins each one outside packages/spec/src/ as an explicit case, alongside the two boundary controls (a private member's src/ and a published package file outside src/, both outside the face).

The battery was rewritten and its floor raised from 7 to 21. Every case passes its own publishedFaces, because reconcile defaults to none; the last three cases hold the fixture population against the live enumeration, so the battery cannot be green over a workspace this repo does not have.

Clause 2 and the changeset, both re-measured

Clause-②: no, re-derived rather than inherited. git grep -n "dispatcher-error-vocabulary" HEAD -- packages/runtime/src/index.ts returns zero: the module is not exported from the published entry point. Confirmed from the dist side after building @objectstack/runtime: boot-refusal, CodeVerdict, UNREGISTERED_CODE_SITES, SANDBOX_AUTHORED_LIMB and dispatcher-error-vocabulary each hit 0 of the 6 files in packages/runtime/dist/, against positive controls MissingDriverPackageError and resolveProjectDatabaseUrl at 4 files each. Deleting a member of a type that does not ship, plus a strictly stricter gate, moves no published face.

Re-derived again after the TSDoc edit, because that edit lands in packages/spec/src/**: the ledger holds the same 282 unique codes before and after it (279 before #16783 landed and added three), every changed line in that file is a TSDoc * line with no code line moved, and the generated api-surface and spec-changes.json artifacts did not move — check:generated found only the docs page stale. Prose in a shipped file widens no accept set and adds no key, so Clause-② stays no. ⚠️ The clause-2 enqueue gate's PATH leg will nonetheless fire, because the diff now touches packages/spec/src/**; that is expected and is the PM's to handle with a contract-review-tier review before enqueue. It is not a reason to declare yes, and the prose was not left false to dodge it.

skip-changeset, measured on both halves as the lane requires. Path test: 70 published manifests examined, none carries a files[] entry that could ship a root scripts/ path, and the root manifest is private: true; @objectstack/runtime's files[] is ['dist','README.md','CHANGELOG.md'], so its src/ is not shipped. Dist test: the symbol readings above. Nothing published moves.

Verification

Final head 07cb189d. origin/main moved twice under this branch and was merged both times through bash scripts/pm/os-regen-merge.sh; the second merge (b3c14d2a) brought #16783, one of the two PRs that contended this ledger file. Its regions (:677-728) and this branch's (:106-136) are disjoint and the source merged with no conflict; the generated page had both sides changed, so the script restored main's side and step 4 regenerated it (07cb189d), after which the page carries #16783's three new rows and this branch's corrected TSDoc together. Verified after the merge rather than assumed: all fourteen of PR #16879's registrations still present (14/14), ledger at 282 unique codes, boot-refusal still absent from CodeVerdict, and this branch's derivePublishedFaces and corrected TSDoc both intact. All readings below are from 07cb189d.

  • Gate set derived with node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, letting it take the change set from the merge base itself: 111 families over the four changed paths (the TSDoc and docs paths took it from 64 to 111). Re-derived and re-run in full on the final head after the second merge. 110 exit 0. Reconciled with --ran: "111 derived famil(ies) accounted for — 111 run, 0 NOT-MEASURED".
  • Three gates first failed as prerequisites, not findings, and were made to measure rather than written off: check:doc-formula-expressions and check:doc-security-posture exited 3 naming @objectstack/formula and @objectstack/lint as unbuilt, and check:skill-examples exited 1 asking for @objectstack/client-react — building those three closures took all three to exit 0.
  • pnpm check:dispatcher-error-vocabulary exit 0 — self-test "10 shapes + 344 assertions OK", production run "52 unregistered code-stamping site(s), all classified; 2 awaiting a ledger entry (spec: register the dispatcher conformance gate's reported error codes in ERROR_CODE_LEDGER (spec half of the #8087 ruling) #8846)".
  • pnpm check:dual-build-cjs-loads exit 3, PREREQUISITE NOT MET — it reads built output and some packages have no dist/ in this worktree; its own text says "This is NOT a pass: nothing was measured". Recorded as NOT MEASURED, not as a failure, and not as a pass either. Its self-test passed (93 cases). Whole-tree family, CI owns it.
  • Dependency closure and the affected package, under scripts/pm/os-verify-lock.sh: pnpm --workspace-concurrency=2 --filter '@objectstack/runtime...' build exit 0, pnpm --filter @objectstack/runtime typecheck exit 0, pnpm --filter @objectstack/runtime exec vitest run --maxWorkers=2 src/domains/actions-validation-envelope.test.ts exit 0. Wrapper VERDICT command-exit 0.
  • The edited gate script ships no *.test.ts; its tests are its --self-test, wired into check:dispatcher-error-vocabulary and run above. git grep of the script's filename across test files finds one carrier, packages/spec/src/api/error-code-ledger.test.ts — run, 20/20 passed.
  • Full repo lint, not a narrowing: eslint . --no-inline-config exit 0 over 6398 files, 0 errors, 0 warnings, with both changed files confirmed present in the linted set.

Acceptance notes

Not filed, recorded here.

The error-code-ledger.zod.ts prose is fixed in this PR, not deferred — see the patch-round section above. An earlier revision of this PR argued it could be left because it "describes a case that can no longer arise"; that argument was wrong for :109-113, which describes where a live mechanism lives, and it is withdrawn.

Still out of scope and genuinely deferred: the three source comments in packages/core (×2) and packages/drivers/driver-sql (×1) that state the pre-ruling position. #16884 is already open on exactly those and explicitly names the stale boot-refusal pointer, so nothing was re-filed. Successor: #16884.

packages/spec/src/stack.zod.ts mentions the boot-refusal class but qualifies itself with "on the tree this landed against", so it stays true as a historical reading and needs no change. Successor: none.

The sandbox-authored verdict is now refused everywhere in the published face, which follows from "refusing exactly the verdicts the spec face refuses today except pending-registration". No row on this tree carries it — the sandbox limb is outside the scan by construction — so this is a widening with no live subject. It was not retired: that is not ruled here.

🤖 Generated with Claude Code

https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH


Generated by Claude Code

…ed package and retire boot-refusal

`check-dispatcher-error-vocabulary` refused a site classified away from
registration only under `packages/spec/src/` — the one package #16449 had
measured. The #16404 ruling it enforces is about every code that ships in
`dist`, so the refusal now covers every published workspace member's `src/`,
enumerated from the manifests through the shared workspace parse and held
against a floor so a broken enumeration refuses instead of going quiet.

`packages/spec/src/` keeps the stricter verdict set. Outside it,
`pending-registration` keeps a named, dated allowance owed to #8846, so the two
rows whose disposition belongs to #8846 / #9460 do not red on a card that is
not theirs; every run prints how many rows still stand on it.

With the whole scan population inside the face, `boot-refusal` is a verdict no
row can legally carry, so it retires from `CodeVerdict`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH
@os-bill os-bill added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 9, 2026 — with Claude
@github-actions github-actions Bot added the size/m label Sep 9, 2026
@github-actions

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

2 anchor(s) derived from 2 changed package(s); no hand-written page names any of them. ⚠️ 1 changed file(s) yielded no anchor (packages/spec/src/api/error-code-ledger.zod.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/spec/src/api/error-code-ledger.zod.ts) — pages documenting those are invisible to this run
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 139 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 71629a1513bcddaa76bd3435d9f18559cc473bbapackageMentionDocs.

Which tree this was computed on

This run read content/docs from 825136a678bd48bee15421848b748db4f287231c — the merge of head 0292e14f7a7c3ff7a768414afb61d008baf9dcf8 into base 71629a1513bcddaa76bd3435d9f18559cc473bba, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 825136a678bd48bee15421848b748db4f287231c && git checkout 825136a678bd48bee15421848b748db4f287231c
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 71629a1513bcddaa76bd3435d9f18559cc473bba 0292e14f7a7c3ff7a768414afb61d008baf9dcf8 && git checkout -B drift-repro 71629a1513bcddaa76bd3435d9f18559cc473bba && git merge --no-ff 0292e14f7a7c3ff7a768414afb61d008baf9dcf8

node scripts/docs-audit/affected-docs.mjs --json 71629a1513bcddaa76bd3435d9f18559cc473bba

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

…e verdict

`packages/spec` ships `src/**/*.zod.ts` in its `files[]`, so the ledger's
TSDoc is published prose. One sentence said the dispatcher vocabulary's
`boot-refusal` verdict records reachability for codes not yet registered and
pointed at the file this branch deletes it from — a live-mechanism claim, not
a case that can no longer arise, so a reader would follow the pointer and find
nothing. It now records the retirement and names what replaced it.

The paragraph below it claimed only `packages/spec/src/**` is held
mechanically. That understated the rule after the widening, so it now states
the published face, the stricter spec sub-face and the dated #8846 allowance,
with both finding kinds named.

The mirrored reference page is regenerated with `check:generated --fix`
(`gen:docs`), never by hand.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH
@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation and removed size/m labels Sep 9, 2026
… main

`os-regen-merge.sh` step 4. Both sides had changed the generated page, so
step 2 restored main's side and this regenerates it from the merged source:
the reference now carries #16783's three new rows and this branch's corrected
TSDoc together.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH
…workspace glob non-empty

The changeset half is the binding correction: `packages/spec` ships
`src/**/*.zod.ts`, so the ledger's TSDoc goes out in the tarball verbatim and
a docs-only edit to it publishes. `skip-changeset` was wrong the moment that
file entered the diff; this adds the `@objectstack/spec: patch` the lane's
five precedents on this head all carry for the same shape.

Also from the same review, non-binding:

- The published face's floor could not catch a lost workspace glob and the
  docblock implied it could. Measured per glob on this tree: the largest is
  `packages/*` at 31 of 88 members, so losing it entirely still leaves 57 —
  over any floor low enough not to red on ordinary churn — and
  `expandWorkspaceGlob` returns `[]` for a vanished parent silently.
  `emptyWorkspaceGlobs` now refuses that case by name, the floor's docblock
  states what it can and cannot see, and a self-test case asserts the
  arithmetic in the direction that keeps the pin necessary.
- The TSDoc said the gate refuses every verdict that parks a site away from
  registration, which over-generalises the allowance stated eight lines below
  it. It now names `pending-registration` and its scope.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH
@os-bill os-bill removed the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

spec: register the 14 remaining door: 'none' (boot-refusal) codes that ship in dist — the rest of the #16404 class after #16449

2 participants